/*
 * Copyright (c) 2010, VeRSI Consortium
 *   (Victorian eResearch Strategic Initiative, Australia)
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the VeRSI, the VeRSI Consortium members, nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package versi.vitro.oaiserviceprovider.model;

import static versi.vitro.oaiserviceprovider.application.Environment.RESOURCE_MODELS;
import static versi.vitro.oaiserviceprovider.application.Environment.VITRO_HOST;
import static versi.vitro.oaiserviceprovider.application.Environment.VITRO_PORT;
import static versi.vitro.oaiserviceprovider.application.Environment.VITRO_PROTOCOL;
import static versi.vitro.oaiserviceprovider.application.Environment.VITRO_WEBAPP_NAME;
import static versi.vitro.oaiserviceprovider.helper.Constant.ADD_INFO_MODEL_NAME;
import static versi.vitro.oaiserviceprovider.helper.Constant.ANDS_TRIPLES_MODEL_NAME;
import static versi.vitro.oaiserviceprovider.helper.Constant.AND_HARVEST_OUT_MODEL_NAME;
import static versi.vitro.oaiserviceprovider.helper.Constant.HANDLE_MODEL_NAME;
import static versi.vitro.oaiserviceprovider.helper.Constant.NEW_ANDS_TRIPLES_MODEL_NAME;
import static versi.vitro.oaiserviceprovider.helper.Constant.TIMESTAMP_MODEL_NAME;
import static versi.vitro.oaiserviceprovider.helper.Constant.VITRO_JENA_DEFAULT_MODEL;
import static versi.vitro.oaiserviceprovider.service.handle.api.HandleServiceConstant.andsHandleServiceAppId;
import static versi.vitro.oaiserviceprovider.service.handle.api.HandleServiceConstant.andsHandleServiceAuthDomain;
import static versi.vitro.oaiserviceprovider.service.handle.api.HandleServiceConstant.andsHandleServiceIdentifier;
import static versi.vitro.oaiserviceprovider.service.handle.api.HandleServiceConstant.andsHandleServiceUrl;
import static versi.vitro.oaiserviceprovider.service.handle.api.HandleServiceConstant.vitroHandleDataType;

import java.io.ByteArrayOutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.log4j.Logger;

import versi.vitro.oaiserviceprovider.application.Environment;
import versi.vitro.oaiserviceprovider.helper.GeneralHelper;
import versi.vitro.oaiserviceprovider.service.handle.api.HandleService;
import versi.vitro.oaiserviceprovider.service.jena.api.JenaService;

import com.hp.hpl.jena.rdf.model.ModelMaker;
import com.hp.hpl.jena.rdf.model.NodeIterator;
import com.hp.hpl.jena.rdf.model.Property;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.ResIterator;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;

/**
 * Service implementing {@link ExportableModelGenerator}.
 */
public class AndsExportableModelGeneratorImpl implements ExportableModelGenerator {

    /** log4j instance. */
    static final Logger LOGGER = Logger.getLogger(AndsExportableModelGeneratorImpl.class);

    /** General utility class. */
    public GeneralHelper generalHelper;

    /** Jena Service. */
    public JenaService jenaService;

    /** Handle Service. */
    public HandleService handleService;

    /** SPARQL statement of getting extra information which used as a source of creating the ANDS triples model. */
    private String additionalInfoSparql;

    /** SPARQL statement of getting ANDS triples model. */
    private String andsTripleModelSparql;

    /** SPARQL statement of generating the timestamp of harvestable records. */
    private String timeStampSparql;

    /** SPARQL statement of getting reified harvest named records. */
    private String reifiedHarvestNamedSparql;

    /** SPARQL statement of getting all published records without a handle. */
    private String publishedRecordsHandleGeneratorSparql;

    /** Date time Format used in OAI-PMH records. */
    private DateFormat dateFormat;

    /** Ensure handle value is associated with each havestable record.*/
    private boolean isAutoGenerateHandle;

    /** Export result should use additional info model. */
    private Boolean shouldUseAddInfoModel = Environment.SHOULD_USE_ADDITIONAL_INFO_MODEL;

    /**
     * Constructor.
     * 
     * @param jenaService
     *            JENA service.
     * @param generalHelper
     *            General utilities.
     * @param handleService
     *            Handle Service.
     * @param dataTimeFormat
     *            Date time Format used in OAI-PMH records.
     * @param andsTripleModelSparqlPath
     * @param timeStampSparqlPath
     * @param reifiedHarvestNamedSparqlPath
     * @param publishedRecordsHandleGeneratorSparqlPath
     * @param isAutoGenerateHandle
     *            True if automatic generates a handle, otherwise false.
     */
    public AndsExportableModelGeneratorImpl(final JenaService jenaService, final GeneralHelper generalHelper,
	    final HandleService handleService, final String dataTimeFormat, final String andsTripleModelSparqlPath,
	    final String timeStampSparqlPath, final String reifiedHarvestNamedSparqlPath,
	    final String publishedRecordsHandleGeneratorSparqlPath, final String additionalInfoSparqlPath,
	    final String isAutoGenerateHandle) {
	super();
	// Services
	this.jenaService = jenaService;
	this.generalHelper = generalHelper;
	this.handleService = handleService;
	// Configuration
	this.dateFormat = new SimpleDateFormat(dataTimeFormat);
	this.andsTripleModelSparql = generalHelper.readResourceAsString(andsTripleModelSparqlPath);
	this.timeStampSparql = generalHelper.readResourceAsString(timeStampSparqlPath);
	this.reifiedHarvestNamedSparql = generalHelper.readResourceAsString(reifiedHarvestNamedSparqlPath);
	this.publishedRecordsHandleGeneratorSparql = generalHelper
		.readResourceAsString(publishedRecordsHandleGeneratorSparqlPath);
	this.additionalInfoSparql = generalHelper.readResourceAsString(additionalInfoSparqlPath);
	this.isAutoGenerateHandle = Boolean.parseBoolean(isAutoGenerateHandle);
    }

    /**
     * Gets the Jena Service.
     * 
     * @return JenaService Jena Service.
     */
    protected JenaService getJenaService() {
	return jenaService;
    }

    /**
     * Sets the Handle Service.
     * 
     * @param HandleService
     *            Handle Service to set.
     */
    protected void setHandleService(HandleService handleService) {
	this.handleService = handleService;
    }

    /**
     * Gets the Handle Service.
     * 
     * @return HandleService Handle Service.
     */
    protected HandleService getHandleService() {
	return handleService;
    }

    /**
     * Preparation: ensure required models are instantiated
     */
    protected void ensureModelsExisted() {

	boolean hasANDSTriples = Boolean.FALSE;
	boolean hasANDSHarvestOut = Boolean.FALSE;
	boolean hasNewANDSTriples = Boolean.FALSE;
	boolean hasRectifiedHarvest = Boolean.FALSE;
	boolean hasAddInfo = Boolean.FALSE;
	boolean hasHandle = Boolean.FALSE;

	ModelMaker modelMaker = jenaService.getModelMaker();
	Iterator<String> iterator = modelMaker.listModels();
	while (iterator.hasNext()) {

	    String existingModelName = iterator.next();

	    if (isDebugEnabled()) {
		LOGGER.debug(existingModelName.toString() + " size = " + modelMaker.getModel(existingModelName).size());
	    }

	    if (existingModelName.equals(ANDS_TRIPLES_MODEL_NAME)) {
		hasANDSTriples = true;
	    }
	    if (existingModelName.equals(AND_HARVEST_OUT_MODEL_NAME)) {
		hasANDSHarvestOut = true;
	    }
	    if (existingModelName.equals(NEW_ANDS_TRIPLES_MODEL_NAME)) {
		hasNewANDSTriples = true;
	    }
	    if (existingModelName.equals(TIMESTAMP_MODEL_NAME)) {
		hasRectifiedHarvest = true;
	    }
	    if (existingModelName.equals(ADD_INFO_MODEL_NAME)) {
		hasAddInfo = true;
	    }
	    if (existingModelName.equals(HANDLE_MODEL_NAME)) {
		hasHandle = true;
	    }
	}

	if (!hasANDSTriples) {
	    LOGGER.info(ANDS_TRIPLES_MODEL_NAME + " is not found and it will be created");
	    jenaService.createModel(ANDS_TRIPLES_MODEL_NAME);
	}
	if (!hasANDSHarvestOut) {
	    LOGGER.info(AND_HARVEST_OUT_MODEL_NAME + " is not found and it will be created");
	    jenaService.createModel(AND_HARVEST_OUT_MODEL_NAME);
	}
	if (!hasNewANDSTriples) {
	    LOGGER.info(NEW_ANDS_TRIPLES_MODEL_NAME + " is not found and it will be created");
	    jenaService.createModel(NEW_ANDS_TRIPLES_MODEL_NAME);
	}
	if (!hasRectifiedHarvest) {
	    LOGGER.info(TIMESTAMP_MODEL_NAME + " is not found and it will be created");
	    jenaService.createModel(TIMESTAMP_MODEL_NAME);
	}
	if (!hasAddInfo) {
	    LOGGER.info(ADD_INFO_MODEL_NAME + " is not found and it will be created");
	    jenaService.createModel(ADD_INFO_MODEL_NAME);
	}
	if (!hasHandle) {
	    LOGGER.info(HANDLE_MODEL_NAME + " is not found and it will be created");
	    jenaService.createModel(HANDLE_MODEL_NAME);
	}
    }

    /**
     * Clears the newANDSTriples model subtract the andsHarvestOut model from
     * the andstriples model and put the result in newANDSTriples
     */
    protected void findDiff() {
	jenaService.clearModel(NEW_ANDS_TRIPLES_MODEL_NAME);
	jenaService.subtractModels(ANDS_TRIPLES_MODEL_NAME, AND_HARVEST_OUT_MODEL_NAME, NEW_ANDS_TRIPLES_MODEL_NAME);
    }

    /** Gets published records. */
    private List<String> getPublishedRecords() {

	//String sourceModel = VITRO_JENA_DEFAULT_MODEL;
	String sourceModel = HANDLE_MODEL_NAME;
	byte[] resultByte = jenaService.executeSparqlQuery(publishedRecordsHandleGeneratorSparql, sourceModel);
	if (resultByte.length != 0) {
	    return Arrays.asList(new String(resultByte).split(","));
	}
	return null;
    }

    /** Generates handles for each published records. */
    protected void generateHandleForPublishedRecords() {

	List<String> datasetRecordsList = getPublishedRecords();

	if (datasetRecordsList != null) {

	    String vivoURL = VITRO_PROTOCOL + "://" + VITRO_HOST + ":" + VITRO_PORT + "/" + VITRO_WEBAPP_NAME;

	    handleService.configureANDSHandleService(andsHandleServiceUrl, andsHandleServiceAppId,
		    andsHandleServiceIdentifier, andsHandleServiceAuthDomain);

	  //  int value = 9670;
	    for (String recordID : datasetRecordsList) {

		String vivoRecordURL = vivoURL + "/individual?uri=" + recordID;
		LOGGER.info("Minting handle of "+vivoRecordURL);
		String handle = handleService.pidCreateHandle("URL", vivoRecordURL);
		//String handle="10378.3/"+value;
		
		jenaService.addRDF(HANDLE_MODEL_NAME, recordID, vitroHandleDataType, handle);
		LOGGER.info("A handle has been feed back to '" + HANDLE_MODEL_NAME + "' model.");
		//value++;
	    }

	} else {

	    LOGGER.info("All harvestable dataset have already been associated with a handle. ");
	}

    }

    /** {@inheritDoc} */
    public void generateExportModel() {

/*	ByteArrayOutputStream out = jenaService.getModelAsOutputStream("UoMbackgroundData", "N-Triples");
	byte[] byteArray = out.toByteArray();
	generalHelper.writeByteArrayToLocalFile(byteArray, "UoMbackgroundData.xml");*/
	
	if (isAutoGenerateHandle) {
	    generateHandleForPublishedRecords();
	}
	ensureModelsExisted();
	findHarvestReadyRecords();
	addTimestamp();
	generateOutputModel();

	if (isDebugEnabled()) {
	    LOGGER.debug("========================");
	    jenaService.printAllModelsSize();
	    LOGGER.debug("========================");
	}
    }

    /** Finds all record are ready to be harvested. */
    private void findHarvestReadyRecords() {
	// 1) clear the andstriples model
	jenaService.clearModel(ANDS_TRIPLES_MODEL_NAME);
	LOGGER.info("Step 1. clearing the output model is completed! ");
	String[] addInfoModelSourceStr=null;
	String[] andsTripleSourceStr=null;
	if (shouldUseAddInfoModel) {
	    
	    if (RESOURCE_MODELS != "") {

		addInfoModelSourceStr = RESOURCE_MODELS.split(",");
		 LOGGER.info("RESOURCE_MODELS =" + RESOURCE_MODELS);
		     jenaService.populateModelwithSPARQL(additionalInfoSparql, addInfoModelSourceStr, ADD_INFO_MODEL_NAME, true);
		    LOGGER.info("Step 2. populating extra info to addInfo model is completed!");
		    String concatString = ADD_INFO_MODEL_NAME+","+RESOURCE_MODELS;
		    LOGGER.debug("conString =" + concatString);
		    andsTripleSourceStr = concatString.split(",");
	    }

	
	} else {
	    LOGGER.info("Step 2. addInfo model is not used!");
	    andsTripleSourceStr =  RESOURCE_MODELS.split(",") ;
	}
		
	jenaService.populateModelwithSPARQL(andsTripleModelSparql, andsTripleSourceStr, ANDS_TRIPLES_MODEL_NAME, true);
	LOGGER.info("Step 3. populating ANDS model completed!");

	// 3) clear the newANDSTriples model subtract the andsHarvestOut model
	// from the andstriples model and put the result in newANDSTriples
	findDiff();
	LOGGER.info("Step 4. building a new ANDS Triple model completed!");

    }

    protected void addTimestamp() {
	// 4) clear the reifiedHarvest model create reified statements with
	// Harvest:firstHarvested dates from the newANDSTriples model into the
	// reifiedHarvest model using the following CONSTRUCT query
	// jenaService.clearModel(RECTIFIED_HARVEST_MODEL_NAME);

	timeStampSparql = addCurrentTimeStamp(timeStampSparql);

	String[] timeStampsourceStr = new String[] { NEW_ANDS_TRIPLES_MODEL_NAME };
	jenaService.populateModelwithSPARQL(timeStampSparql, timeStampsourceStr, TIMESTAMP_MODEL_NAME, false);
	LOGGER.info("Step 5. building a timestamp model completed!");
	jenaService.printAllModelsSize();

	// 5)name the newly created blank node triples and add them to the
	// rectifiedHarvested model;
	// String[] sourceStr1 = new String[] { RECTIFIED_HARVEST_MODEL_NAME };
	// insertHarvestedDate("http://researchDataRegistry.unimelb.edu.au/ANDSHARVEST#",
	// sourceStr1,
	// RECTIFIED_HARVESTED_NAMED_MODEL_NAME);
	// LOGGER.info("Step 5. rename blank nodes within the new built model completed!");
    }

    protected void generateOutputModel() {

	// 6) clear the andsHarvestOut model, and repopulate it with the
	// following CONSTRUCT over the andstriples and rectifiedHarvestedNamed
	// models;
	jenaService.clearModel(AND_HARVEST_OUT_MODEL_NAME);

	String[] outputSourceStr = new String[] { ANDS_TRIPLES_MODEL_NAME, TIMESTAMP_MODEL_NAME };
	jenaService.populateModelwithSPARQL(reifiedHarvestNamedSparql, outputSourceStr, AND_HARVEST_OUT_MODEL_NAME, false);
	LOGGER.info("Step 6. personal profiles and web pages are attached!");

	jenaService.deleteModel(ADD_INFO_MODEL_NAME);
	jenaService.deleteModel(ANDS_TRIPLES_MODEL_NAME);
	jenaService.deleteModel(NEW_ANDS_TRIPLES_MODEL_NAME);
    }

    /** Adds a current Date Time in a String. */
    protected String addCurrentTimeStamp(String inString) {
	return inString.replaceAll("CurrentDateTime", currentDateTimeAsString());
    }

    /** Gets current date time as a String. */
    protected String currentDateTimeAsString() {
	Date date = new Date();
	
	
	return dateFormat.format(date);
    }

    protected boolean isDebugEnabled() {
	return false;
	//return LOGGER.isDebugEnabled();
    }
}
